Guía maestra: Integración de APIs de Zoom con PHP + MySQL

Última revisión: 10 de agosto de 2025 — Enfoque práctico para Server-to-Server OAuth, OAuth 2.0 user-managed, endpoints REST v2, y Webhooks.

PHP 8.x MySQL 8.x TLS 1.2+ OAuth 2.0 Webhooks

Resumen ejecutivo

Zoom expone una API REST v2 con base https://api.zoom.us/v2, autenticada mediante OAuth 2.0 en dos variantes: Server-to-Server OAuth (sin interacción de usuario, ideal para servidores) y OAuth user-managed (consentimiento del usuario y uso de refresh tokens). Además, ofrece Webhooks para recibir eventos (p. ej., meeting.started, recording.completed).

Familias de endpoints frecuentes (no exhaustivo): Reuniones (Meetings), Usuarios, Webinars, Grabaciones en la nube, Chat/Team Chat, Phone, Roles/Cuentas.

Mapa de servicios & ambientes

Requisitos previos

  1. Cuenta Zoom con acceso al Marketplace (rol Admin/Owner recomendado).
  2. Crear una app en Marketplace:
    • Server-to-Server OAuth (recomendado para backend sin usuarios): copia Account ID, Client ID, Client Secret, y define scopes.
    • o OAuth user-managed (si necesitas actuar en nombre de usuarios): configura redirect URI y scopes.
  3. Webhook Secret Token (si usarás Webhooks) y endpoint público HTTPS.
  4. Servidor PHP 8.x con extensiones curl, openssl, json.

Esquema MySQL base

-- Tokens (Server-to-Server, OAuth user-managed)
CREATE TABLE zoom_tokens (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  flow ENUM('S2S','OAUTH_USER') NOT NULL,
  access_token VARCHAR(400) NOT NULL,
  expires_at DATETIME NOT NULL,
  refresh_token VARCHAR(400) NULL,     -- sólo para OAuth user-managed
  scopes TEXT NULL,
  meta JSON NULL,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL
);

-- Reuniones programadas
CREATE TABLE zoom_meetings (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  zoom_meeting_id BIGINT NULL,         -- "id" numérico de Zoom
  uuid VARCHAR(255) NULL,
  host_user VARCHAR(200) NOT NULL,     -- email o "me"
  topic VARCHAR(255) NOT NULL,
  start_time DATETIME NULL,
  duration INT NULL,
  timezone VARCHAR(64) NULL,
  join_url TEXT NULL,
  start_url TEXT NULL,
  payload JSON NULL,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL
);

-- Webhooks recibidos (auditoría)
CREATE TABLE zoom_webhooks (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  event VARCHAR(120) NOT NULL,
  delivery_ts BIGINT NOT NULL,         -- unix ts de header (si aplica)
  signature VARCHAR(256) NULL,         -- x-zm-signature
  body MEDIUMTEXT NOT NULL,            -- raw body
  processed TINYINT(1) DEFAULT 0,
  created_at DATETIME NOT NULL
);

Utilitarios PHP

<?php
// Variables de entorno recomendadas:
$ZOOM_ACCOUNT_ID   = getenv('ZOOM_ACCOUNT_ID');   // sólo S2S
$ZOOM_CLIENT_ID    = getenv('ZOOM_CLIENT_ID');
$ZOOM_CLIENT_SECRET= getenv('ZOOM_CLIENT_SECRET');
$ZOOM_WEBHOOK_SECRET = getenv('ZOOM_WEBHOOK_SECRET'); // para verificación

function http_post_json($url, $headers, $payload){
  $ch = curl_init($url);
  $h = array_merge(['Content-Type: application/json','Accept: application/json'], $headers);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => $h,
    CURLOPT_POSTFIELDS => is_string($payload)? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 60,
  ]);
  $resp = curl_exec($ch);
  $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  $err = curl_error($ch);
  curl_close($ch);
  if ($resp === false) { throw new RuntimeException("cURL error: $err"); }
  return [$http, $resp];
}
?>

Autenticación: Server-to-Server OAuth (recomendado)

Obtén un access_token corto con credenciales de cuenta. No hay refresh token; cuando expire, solicita otro token.

<?php
// 1) Obtener access_token (S2S)
$basic = base64_encode($ZOOM_CLIENT_ID.':'.$ZOOM_CLIENT_SECRET);
$tokenUrl = 'https://zoom.us/oauth/token?grant_type=account_credentials&account_id='.rawurlencode($ZOOM_ACCOUNT_ID);

$ch = curl_init($tokenUrl);
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ['Authorization: Basic '.$basic],
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 30
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($resp === false || $http >= 400) { throw new RuntimeException("Zoom token HTTP $http: ".$resp); }
$data = json_decode($resp, true);
$accessToken = $data['access_token'] ?? null;
$expiresIn   = $data['expires_in'] ?? 0; // segundos
if (!$accessToken) { throw new RuntimeException('No se obtuvo access_token'); }
// Persistir $accessToken y calcular expires_at = NOW() + $expiresIn
?>

Autenticación: OAuth user-managed (con consentimiento)

Usa cuando necesitas actuar en nombre de usuarios finales. Flujo:

  1. Redirige a https://zoom.us/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=URL.
  2. Intercambia code por access_token y refresh_token en POST https://zoom.us/oauth/token con Authorization: Basic.
  3. Renueva con grant_type=refresh_token cuando caduque.

Endpoints clave (ejemplos)

Crear una reunión

POST /users/{userId}/meetings (puedes usar me como userId para el propietario del token). Scopes: meeting:write o meeting:write:admin. Límite típico del endpoint: 100 solicitudes/día (verifica tu cuenta y etiqueta de rate limit).

<?php
$payload = [
  'topic' => 'Demo API',
  'type'  => 2, // 2=Scheduled
  'start_time' => '2025-08-10T17:00:00Z',
  'duration' => 30,
  'timezone' => 'UTC',
  'settings' => [
    'host_video' => false,
    'participant_video' => false,
    'join_before_host' => false,
    'mute_upon_entry' => true
  ]
];

list($http, $resp) = http_post_json(
  'https://api.zoom.us/v2/users/me/meetings',
  ['Authorization: Bearer '.$accessToken],
  $payload
);
if ($http !== 201 && $http !== 200) { throw new RuntimeException("Zoom create meeting HTTP $http: $resp"); }
$meeting = json_decode($resp, true);
// Guarda $meeting['id'], $meeting['join_url'], $meeting['start_url'], $meeting['uuid'], etc.
?>

Obtener/actualizar/eliminar

Listar reuniones del usuario

Webhooks (suscripción a eventos)

Configura una app en Marketplace → Event Subscriptions, añade eventos (p. ej., meeting.started, meeting.ended, recording.completed) y define el Event notification endpoint URL. Usa el Secret Token para verificar la autenticidad.

Validación por firma (recomendada)

Zoom envía cabeceras como x-zm-request-timestamp y x-zm-signature. Calcula el HMAC SHA-256 de la cadena v0:{timestamp}:{raw_body} usando tu Secret Token y compara con x-zm-signature.

<?php
// endpoint: /webhooks/zoom
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_ZM_REQUEST_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_ZM_SIGNATURE'] ?? '';
if (!$ts || !$sig) { http_response_code(400); exit; }

// 1) Prevención de replays (tolerancia 5 min)
if ((time() - (int)$ts) > 300) { http_response_code(403); exit; }

// 2) Verificar firma
$msg   = "v0:$ts:$raw";
$calc  = hash_hmac('sha256', $msg, $ZOOM_WEBHOOK_SECRET);
$valid = hash_equals($sig, $calc);
if (!$valid) { http_response_code(401); exit; }

// 3) Procesar el evento
$event = json_decode($raw, true);
if (($event['event'] ?? '') === 'endpoint.url_validation') {
  // Responder al CRC (challenge-response) si se solicita validación
  $plain = $event['payload']['plainToken'] ?? '';
  $encrypted = hash_hmac('sha256', $plain, $ZOOM_WEBHOOK_SECRET);
  header('Content-Type: application/json');
  echo json_encode(['plainToken' => $plain, 'encryptedToken' => $encrypted]);
  exit;
}

// ... encolar y procesar meeting.started/ended, recording.completed, etc.
http_response_code(200);
?>

Tips: responde <= 3 s; reintenta idempotente; registra timestamp, firma y raw body. Zoom puede revalidar periódicamente el endpoint.

Paso a paso de implementación

Crear app en Marketplace

  • Elige Server-to-Server OAuth (privada) o OAuth (user-managed) según tu caso.
  • Define scopes mínimos necesarios (p. ej., meeting:write).
  • Si usarás Webhooks, copia el Secret Token y configura eventos.

Autenticación

  • S2S: implementa solicitud del token con Basic + account_credentials; cachea hasta expiración.
  • OAuth: implementa autorización, refresco de tokens y rotación segura.

Consumo de API

  • Crea/actualiza/borra reuniones; guarda id, join_url, start_url.
  • Maneja errores y rate limits con backoff exponencial.

Webhooks

  • Verifica firma x-zm-signature; responde a endpoint.url_validation si se solicita.
  • Diseña handlers idempotentes (usa uuid del evento para evitar duplicados).

Operación

  • Monitorea tasas de respuesta, errores, y latencias por endpoint.
  • Registra auditoría con hashes del raw body de webhooks y correlación con reuniones.

Buenas prácticas

Solución de problemas comunes

ProblemaCausa probableAcción
unsupported_grant_type al pedir token S2SGrant incorrecto o método/verbo erróneoUsa POST a /oauth/token?grant_type=account_credentials&account_id=... con Authorization: Basic.
invalid_clientClient ID/Secret o Account ID no coincidenVerifica credenciales copiadas desde la app Marketplace; reintenta con Basic correcto.
401 al llamar /v2Token expirado o scopes insuficientesRenueva token; revisa scopes de la app y del endpoint específico.
Validación de webhook fallaFirma HMAC incorrecta o timestamp fuera de toleranciaConstruye v0:ts:raw_body; compara con x-zm-signature; tolerancia ~5 min.
429 Too Many RequestsRate limit por etiqueta del endpointImplementa backoff; revisa documentación de límites por método.

Anexos & referencias (útiles para el equipo)

Publica esta guía internamente; mantén enlaces a la documentación oficial de Zoom y revisa cambios de scopes/límites por versión.